<template>
  <el-dialog
    v-model="visible"
    title="创建任务"
    width="500px"
    center
    @close="resetForm"
  >
    <el-form ref="taskFormRef" :model="form" :rules="rules" label-width="80px">
      <el-form-item
        v-show="step === 1"
        prop="file"
        :rules="[{ required: true, message: '请上传文件', trigger: 'change' }]"
        label-width="0"
        class="upload-item"
      >
        <el-upload
          v-model:file-list="fileList"
          drag
          action="#"
          :auto-upload="false"
          :on-remove="handleRemove"
          :limit="1"
          :multiple="false"
          :on-exceed="handleExceed"
          :on-change="handleChange"
          class="w-full"
        >
          请拖动文件到此处或<br /><em>点击上传</em>
          <template #tip>
            <div class="el-upload__tip">
              支持 txt、csv、xlsx格式文件，最大10GB
            </div>
          </template>
        </el-upload>
      </el-form-item>
      <el-button
        v-show="step === 1"
        type="success"
        :disabled="!form.file"
        @click="submitFile"
      >
        上传文件
      </el-button>
      <div v-show="step === 1" class="pre-point">
        <span>
          当前：
          <el-text type="primary">{{ fileLine }}</el-text>
          条
        </span>
        <span>
          消耗积分：
          <el-text type="primary">{{ prePrice }}</el-text>
        </span>
        <span>
          我的积分：
          <el-text type="primary">{{ pointsBalance }}</el-text>
          <el-text v-if="unPay" type="danger"> （余额不足，请充值） </el-text>
        </span>
        <el-tooltip effect="dark" content="点击刷新我的积分" placement="top">
          <el-button
            type="success"
            text
            bg
            :icon="Refresh"
            @click="refreshPointsDebounce"
          />
        </el-tooltip>
      </div>

      <!-- 任务标题，可选，不填写则自动生成 -->
      <el-form-item v-show="step === 2" label="任务标题">
        <el-input v-model="form.name" placeholder="不填写则自动生成" />
      </el-form-item>
      <!-- 国家选择，由外部传入的国家列表 -->
      <el-form-item
        v-show="step === 2"
        label="国家"
        prop="country"
        :rules="[{ required: true, message: '请选择国家', trigger: 'change' }]"
      >
        <el-select
          v-model="form.country"
          filterable
          clearable
          :disabled="!!country"
          placeholder="请选择国家"
        >
          <el-option
            v-for="item in countries"
            :key="item.id"
            :label="item.name"
            :value="item.name"
          />
        </el-select>
      </el-form-item>
    </el-form>
    <el-result
      v-show="step === 3"
      icon="success"
      title="校验结果"
      :sub-title="checkMessage"
    />

    <template #footer>
      <el-button
        v-if="step > 1"
        type="danger"
        :icon="ArrowLeftBold"
        @click="prevStep"
      >
        上一步
      </el-button>
      <el-button
        v-if="!!form.dataFile"
        type="success"
        :loading
        @click="nextStep"
      >
        {{ stepTextMap[step] }}
        <el-icon class="el-icon--right"><ArrowRightBold /></el-icon>
      </el-button>
    </template>
  </el-dialog>
  <el-dialog
    v-model="uploading.visible"
    :show-close="false"
    :close-on-click-modal="false"
    :close-on-press-escape="false"
    width="480px"
    align-center
    ><template #header>
      <div style="display: flex; align-items: center; gap: 8px">
        <el-icon><Upload /></el-icon>
        <span>文件上传中</span>
      </div>
    </template>

    <div style="display: flex; flex-direction: column; gap: 12px">
      <div
        style="
          display: flex;
          justify-content: space-between;
          align-items: center;
        "
      >
        <div
          style="
            font-weight: 500;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
            max-width: 320px;
          "
        >
          {{ uploading.fileName }}
        </div>
        <div style="color: #909399">{{ uploading.percent.toFixed(1) }}%</div>
      </div>
      <el-progress
        :percentage="Math.min(100, Math.max(0, Math.round(uploading.percent)))"
        :status="
          uploading.cancelling
            ? 'warning'
            : uploading.percent >= 100
              ? 'success'
              : undefined
        "
        :stroke-width="14"
        striped
        :striped-flow="true"
        :duration="20"
      />
      <div
        style="
          display: flex;
          justify-content: space-between;
          align-items: center;
          color: #909399;
        "
      >
        <span>{{ uploading.statusText }}</span>
        <span v-if="uploading.speedText">{{ uploading.speedText }}</span>
      </div>
    </div>

    <template #footer>
      <el-button
        :disabled="uploading.cancelling || uploading.percent >= 100"
        :loading="uploading.cancelling"
        type="danger"
        @click="cancelLargeUpload"
      >
        取消上传
      </el-button>
    </template>
  </el-dialog>
</template>

<script lang="ts" setup>
import { ref, reactive, computed, nextTick } from "vue";
import type {
  FormInstance,
  FormRules,
  UploadProps,
  UploadFiles,
  UploadFile
} from "element-plus";
import {
  uploadDataFile,
  checkTaskInfo,
  createTask,
  uploadPart,
  initMultipartUpload,
  getMissingParts,
  completeMultipartUpload,
  cancelUpload
} from "@/api/taskApi";
import { getUserServiceTypes } from "@/api/globalConfigApi";
import { ElMessage } from "element-plus";
import { useUserStoreHook } from "@/store/modules/user";
import {
  ArrowLeftBold,
  Upload,
  ArrowRightBold,
  Refresh
} from "@element-plus/icons-vue";
import { debounce } from "lodash-es";

const CHUNK_THRESHOLD = 20 * 1024 * 1024; // 20MB

const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB

const step = ref(1);
const uploading = reactive({
  visible: false,
  percent: 0,
  statusText: "初始化中...",
  speedText: "",
  fileName: "",
  uploadId: "" as string | "",
  abortController: null as AbortController | null,
  startTime: 0,
  loadedBytes: 0,
  totalBytes: 0,
  cancelling: false
});
const openUploadMask = (name: string, totalBytes: number) => {
  uploading.visible = true;
  uploading.percent = 0;
  uploading.statusText = "计算文件哈希...";
  uploading.speedText = "";
  uploading.fileName = name;
  uploading.abortController = null;
  uploading.startTime = Date.now();
  uploading.loadedBytes = 0;
  uploading.totalBytes = totalBytes;
  uploading.cancelling = false;
  uploading.uploadId = "";
};

const closeUploadMask = () => {
  uploading.visible = false;
};

const setUploadStatus = (text: string) => {
  uploading.statusText = text;
};

const formatSpeed = (bytesPerSec: number) => {
  const k = 1024;
  if (bytesPerSec < k) return `${bytesPerSec.toFixed(0)} B/s`;
  if (bytesPerSec < k * k) return `${(bytesPerSec / k).toFixed(1)} KB/s`;
  if (bytesPerSec < k * k * k)
    return `${(bytesPerSec / k / k).toFixed(1)} MB/s`;
  return `${(bytesPerSec / k / k / k).toFixed(1)} GB/s`;
};

const setUploadPercentByChunks = (
  uploadedChunks: number,
  totalChunks: number,
  rawSize: number
) => {
  const approxBytes = Math.min(uploadedChunks * CHUNK_SIZE, rawSize);
  setUploadPercentByBytes(approxBytes);
};

const setUploadPercentByBytes = (uploadedBytes: number) => {
  uploading.loadedBytes = uploadedBytes;
  const pct =
    uploading.totalBytes > 0 ? (uploadedBytes / uploading.totalBytes) * 100 : 0;
  uploading.percent = Math.min(99.9, pct);
  const elapsedSec = (Date.now() - uploading.startTime) / 1000;
  if (elapsedSec > 0 && uploadedBytes > 0) {
    const speed = uploadedBytes / elapsedSec;
    uploading.speedText = formatSpeed(speed);
  }
};
const userStore = useUserStoreHook();
const pointsBalance = computed(
  () => +(userStore.currentUser?.user?.pointsBalance || 0).toFixed(2)
);
const unPay = computed(() => {
  return prePrice.value > pointsBalance.value || pointsBalance.value <= 0;
});

interface TaskForm {
  name: string;
  country: string;
  project: string;
  dataFile: string;
  dataFileName: string;
  file: UploadFile | null;
}
const loading = ref(false);
const fileLine = ref(0);
const props = defineProps<{
  serviceType: string;
  countries: Array<{ id: string; name: string }>;
  country: string;
}>();
const serviceList = ref([]);

const prePrice = computed(() => {
  const currentPrice =
    serviceList.value.find(
      ({ standardCode }) => standardCode === props.serviceType
    )?.price || 0;
  return +(fileLine.value * currentPrice || 0).toFixed(2);
});

const emits = defineEmits(["taskCreated"]);

const refreshPoints = () => {
  userStore.getUserInfo();
};
const refreshPointsDebounce = debounce(refreshPoints, 1000);

const visible = ref(false);

const form = reactive<TaskForm>({
  name: "",
  country: "",
  project: "",
  dataFile: "",
  dataFileName: "",
  file: null
});

// 表单验证规则
const rules: FormRules = {
  country: [{ required: true, message: "请选择国家", trigger: "change" }],
  file: [{ required: true, message: "请上传文件", trigger: "change" }]
};

const taskFormRef = ref<FormInstance>();
const fileList = ref<UploadFiles>([]);

const maxLine = 1000000;
const minLine = 1000;
// 使用 on-change 事件处理文件选择
const handleChange: UploadProps["onChange"] = async (file, fileListParam) => {
  const allowedExtensions = ["txt", "csv", "xlsx"];
  const ext = file.name.split(".").pop()?.toLowerCase();
  if (!ext || !allowedExtensions.includes(ext)) {
    ElMessage.error("文件格式不正确，请上传txt、csv或xlsx文件");
    fileList.value = [];
    form.file = null;
    return;
  }
  form.file = file;
  fileList.value = fileListParam;
};
const submitFile = async () => {
  const file = form.file;
  try {
    if ((file.size as number) > CHUNK_THRESHOLD) {
      await uploadLargeFile(file);
      ElMessage.success("文件上传成功");
    } else {
      await uploadSmallFile();
      ElMessage.success("文件上传成功");
    }
  } catch (e) {
    if (e?.isCancelRequest) {
      ElMessage.success("已取消上传");
    } else {
      ElMessage.error((e as Error)?.message || "上传失败");
    }
    fileList.value = [];
    form.file = null;
    closeUploadMask();
  }
};
const timer = ref(null);
const uploadSmallFile = async () => {
  try {
    uploading.visible = true;
    uploading.percent = 0;
    uploading.statusText = "正在上传...";
    timer.value = setInterval(() => {
      uploading.percent += 10;
      if (uploading.percent >= 100) {
        uploading.percent = 100;
        clearInterval(timer.value);
      }
    }, 1000);
    const formData = new FormData();
    formData.append("file", form.file.raw);
    const { data } = await uploadDataFile(formData);
    const { lineCount, name, filepath } = data;
    fileLine.value = lineCount || 0;
    form.dataFileName = name;
    form.dataFile = filepath;
  } catch (error) {
    console.error("上传文件时出错：", error);
  } finally {
    uploading.percent = 100;
    setTimeout(() => {
      uploading.visible = false;
      if (timer.value) {
        clearInterval(timer.value);
      }
    }, 1000);
  }
};

// 文件删除时清空数据
const handleRemove: UploadProps["onRemove"] = (file, uploadFiles) => {
  form.file = null;
  fileLine.value = 0;
  form.dataFile = "";
  fileList.value = uploadFiles;
};

// 超出上传数量时的提示
const handleExceed = (files: File[]) => {
  ElMessage.error("只能上传一个文件，请先删除已上传的文件");
};
const stepTextMap = {
  1: "下一步",
  2: "校验状态",
  3: "创建任务"
};
const prevStep = () => {
  step.value--;
};

const nextStep = () => {
  switch (step.value) {
    case 1:
      if (!form.dataFile) {
        ElMessage.error("请先上传文件");
      } else {
        step.value++;
      }
      break;
    case 2:
      if (form.country) {
        checkStatus();
      } else {
        ElMessage.error("请选择国家");
      }
      break;
    case 3:
      submitForm();
      break;
  }
};

const cacheKey = ref("");
const checkMessage = ref("");
const checkStatus = async () => {
  try {
    loading.value = true;
    const { file, ...reset } = form;
    const { data } = await checkTaskInfo(reset);
    const { message, redisKey, status } = data;
    if (status === "PENDING_CONFIRMATION") {
      checkMessage.value = message;
      cacheKey.value = redisKey;
      step.value++;
    } else {
      ElMessage.error({
        message,
        duration: 3000
      });
    }
  } finally {
    loading.value = false;
  }
};
// 重置表单
const resetForm = async () => {
  step.value = 1;
  fileList.value = [];
  form.file = null;
  form.dataFile = "";
  form.dataFileName = "";
  fileLine.value = 0;
  form.name = "";
  form.country = props.country || "";
  form.project = props.serviceType || "";
  await nextTick();
  taskFormRef.value?.resetFields();
  taskFormRef.value?.clearValidate();
};
// 提交任务
const submitForm = async () => {
  try {
    loading.value = true;
    await createTask(cacheKey.value);
    emits("taskCreated");
    visible.value = false;
  } catch (error) {
    console.error("提交任务时出错：", error);
  } finally {
    loading.value = false;
  }
};

// 计算文件 SHA-1（与 uploader.js 一致，优先内容哈希，失败降级）

const calculateFileHash = async (file: File): Promise<string> => {
  // 优先计算“基于文件内容”的SHA-1，以便与后端标准哈希路径一致
  try {
    if (
      window.crypto &&
      window.crypto.subtle &&
      typeof file.arrayBuffer === "function"
    ) {
      const buffer = await file.arrayBuffer();
      const hashBuffer = await crypto.subtle.digest("SHA-1", buffer);
      const hashArray = Array.from(new Uint8Array(hashBuffer));
      const hashHex = hashArray
        .map(b => b.toString(16).padStart(2, "0"))
        .join("");
      return hashHex;
    }
  } catch (err) {
    console.warn(
      "⚠️ 内容SHA-1计算失败，降级为元数据哈希: " +
        (err && err.message ? err.message : err)
    );
  }

  // 兼容降级：环境不支持或内存不足时，退回到“基于文件名+大小+mtime”的伪哈希
  try {
    const fallback = btoa(file.name + file.size + file.lastModified)
      .replace(/[^a-zA-Z0-9]/g, "")
      .substring(0, 40)
      .padEnd(40, "0");
    return fallback;
  } catch (_) {
    // 最终兜底
    return String(file.size).padStart(40, "0").substring(0, 40);
  }
};

// 将缺失分片列表格式化（日志友好，可按需使用）

const formatMissingParts = (list: number[]) => {
  if (!Array.isArray(list) || list.length === 0) return "";

  const maxPreview = 10;

  const head = list.slice(0, maxPreview).join(",");

  return list.length > maxPreview ? `${head}, …` : head;
};

// 向后端上传单个分片

const uploadChunk = async (
  uploadId: string,
  partNumber: number,
  chunk: Blob,
  controller: AbortController
) => {
  const formData = new FormData();
  formData.append("chunk", chunk);
  formData.append("partNumber", String(partNumber));
  const { message, data, code } = await uploadPart(
    uploadId,
    formData,
    controller.signal
  );
  if (code !== 200) {
    throw new Error(message || `分片${partNumber}上传失败`);
  }
  return data;
};

// 大文件分片上传主流程（返回与小文件相同的 data DTO）

const uploadLargeFile = async (elFile: UploadFile) => {
  const raw = elFile.raw as File;

  if (!raw) throw new Error("文件无效");
  // 打开蒙层
  openUploadMask(raw.name, raw.size);

  // Step 1. 计算哈希
  setUploadStatus("计算文件哈希...");

  const fileHash = await calculateFileHash(raw);

  const localKey = `upload:session:${fileHash}`;

  // Step 2. init 会话（支持命中已存在文件）

  let uploadId = localStorage.getItem(localKey) || "";

  if (!uploadId) {
    const parasm = {
      fileName: raw.name,
      fileSize: raw.size,
      fileHash
    };
    const { code, message, data } = await initMultipartUpload(parasm);

    if (code !== 200) {
      throw new Error(message || "初始化上传失败");
    }

    // 命中已存在文件：后端直接返回完整 DTO（包含 filepath）

    if (data.filepath && !data.uploadId) {
      // 与小文件一致的赋值

      const { name, filepath, lineCount } = data;

      fileLine.value = lineCount || 0;

      form.dataFileName = name || raw.name;

      form.dataFile = filepath;

      closeUploadMask();

      return data;
    }

    uploadId = data?.uploadId || (typeof data === "string" ? data : null);

    if (!uploadId) {
      throw new Error("初始化返回无效的 uploadId");
    }

    localStorage.setItem(localKey, uploadId);
  }
  uploading.uploadId = uploadId;
  // Step 3. 计算总分片数，查缺补漏

  const totalChunks = Math.ceil(raw.size / CHUNK_SIZE);

  // 健康检查：如果会话无效，重新 init

  try {
    await getMissingParts(uploadId);
  } catch {
    localStorage.removeItem(localKey);

    const { code, message, data } = await initMultipartUpload({
      fileName: raw.name,
      fileSize: raw.size,
      fileHash
    });
    if (code !== 200) {
      throw new Error(message || "初始化上传失败");
    }

    // 命中已存在文件：后端直接返回完整 DTO（包含 filepath）

    if (data.filepath && !data.uploadId) {
      // 与小文件一致的赋值

      const { name, filepath, lineCount } = data;

      fileLine.value = lineCount || 0;

      form.dataFileName = name || raw.name;

      form.dataFile = filepath;
      closeUploadMask();

      return data;
    }

    uploadId = data.uploadId || data;
    localStorage.setItem(localKey, uploadId);
    uploading.uploadId = uploadId;
  }

  // Step 4. 循环上传缺失分片

  // 可在此处集成进度 UI（Element Plus 的 message、progress 等），此处简化为控制台

  // 你也可以在页面加一个进度条并注入 setProgress(percent)

  let uploadedChunks = 0;

  while (true) {
    setUploadStatus("校验缺失分片...");
    const { code, data } = await getMissingParts(uploadId);

    if (!(code === 200 && data)) {
      throw new Error("缺失分片校验失败");
    }

    const missing: number[] = Array.isArray(data.missingParts)
      ? data.missingParts
      : [];

    uploadedChunks = data.actualParts || 0;
    // 已有分片进度
    setUploadPercentByChunks(uploadedChunks, totalChunks, raw.size);

    if (missing.length === 0) break;
    // 为当前批次准备 AbortController
    const controller = new AbortController();
    uploading.abortController = controller;

    // 顺序上传缺失分片（可按需并发，这里保持与后端接口一致、控制简单）

    for (const partNumber of missing) {
      if (controller.signal.aborted) throw new Error("上传已取消");
      setUploadStatus(`上传分片 ${uploadedChunks + 1}/${totalChunks} ...`);
      const start = (partNumber - 1) * CHUNK_SIZE;

      const end = Math.min(start + CHUNK_SIZE, raw.size);

      const chunk = raw.slice(start, end);

      await uploadChunk(uploadId, partNumber, chunk, controller);

      uploadedChunks++;

      setUploadPercentByChunks(uploadedChunks, totalChunks, raw.size);
    }
  }

  // Step 5. 最终校验
  setUploadStatus("最终校验...");
  const { code, data: finalData } = await getMissingParts(uploadId);

  if (
    !(
      code === 200 &&
      Array.isArray(finalData.missingParts) &&
      finalData.missingParts.length === 0
    )
  ) {
    const preview = finalData?.missingParts
      ? formatMissingParts(finalData.missingParts)
      : "";

    throw new Error(`仍存在缺失分片，请重试上传完成 ${preview}`);
  }

  // Step 6. 合并
  setUploadStatus("合并分片...");

  const {
    code: completeCode,
    data: completeResponse,
    message
  } = await completeMultipartUpload(uploadId, {
    fileName: raw.name,
    fileSize: raw.size
  });

  if (completeCode !== 200) {
    throw new Error(message || "完成上传失败");
  }

  const { name, filepath, lineCount } = completeResponse;

  fileLine.value = lineCount || 0;

  form.dataFileName = name || raw.name;

  form.dataFile = filepath;
  uploading.percent = 100;
  uploading.statusText = "上传完成";
  setTimeout(closeUploadMask, 600);

  // 清理本地会话

  localStorage.removeItem(localKey);

  return completeResponse;
};
const cancelLargeUpload = async () => {
  if (!uploading.visible) return;
  uploading.cancelling = true;
  try {
    // 1) 终止当前请求
    if (uploading.abortController) {
      uploading.abortController.abort();
    }
    // 2) 通知后端取消会话（软取消）
    if (uploading.uploadId) {
      // 你未封装 cancel API，这里使用 fetch。若你有 cancel 接口封装，可替换。
      await cancelUpload(uploading.uploadId);
    }
  } finally {
    uploading.cancelling = false;
    uploading.visible = false;
    uploading.percent = 0;
    uploading.statusText = "已取消";
    uploading.speedText = "";
    uploading.fileName = "";
    uploading.uploadId = "";
    uploading.abortController = null;
  }
};
getUserServiceTypes().then(({ data }) => {
  serviceList.value = data;
});
const open = () => {
  visible.value = true;
  form.project = props.serviceType;
  form.country = props.country;
};

defineExpose({
  open
});
</script>

<style scoped>
.pre-point {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 20px;
}
.upload-item {
  :deep(.el-form-item__content) {
    justify-content: center;
  }
}
</style>
